package my.mbean.web;

import com.alibaba.fastjson.JSON;
import my.mbean.MBeanAutoConfiguration;
import my.mbean.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import org.springframework.web.method.annotation.ModelMethodProcessor;
import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.UrlPathHelper;

import javax.annotation.PostConstruct;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static my.mbean.web.MBeanController.PARAME_CONTEXTID;


/**
 * Created by xnat on 17/5/7.
 */
public class RequestDispatcher {
    public static final String URI_TEMPLATE_VARIABLES_ATTRIBUTE      = "uriTemplateVariables";
    public static final String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = "pathWithinHandlerMapping";

    static final String ROOT_PATH = "rootPath";

    protected final     Logger log                                   =  LoggerFactory.getLogger(getClass());
    @javax.annotation.Resource
    private MBeanController                        mBeanController;
    @javax.annotation.Resource
    private ApplicationContext                     ctx;
    private HandlerMethodArgumentResolverComposite argumentResolvers;
    private Map<String, HandlerMethod>             handlerMethodMap;
    private UrlPathHelper                          urlPathHelper;
    private AntPathMatcher                         pathMatcher;
    private String                                 internalStaticResourcePathPrefix;
    private Map<String, String>                    templateCache;

    @PostConstruct
    private void init() {
        urlPathHelper = new UrlPathHelper();
        pathMatcher = new AntPathMatcher();
        handlerMethodMap = new HashMap<>();
        internalStaticResourcePathPrefix = Utils.buildClassPath(MBeanAutoConfiguration.class, "view/resource/");
        templateCache = new WeakHashMap<>();
        initHandlerMethods();
        initArgumentResolvers();
    }


    protected void initArgumentResolvers() {
        argumentResolvers = new HandlerMethodArgumentResolverComposite()
                .addResolver(new RequestParamMapMethodArgumentResolver())
                .addResolver(new ModelMethodProcessor())
                .addResolver(new HandlerMethodArgumentResolver() {
                    @Override
                    public boolean supportsParameter(MethodParameter parameter) {
                        Class<?> paramType = parameter.getParameterType();
                        return (ServletRequest.class.isAssignableFrom(paramType) ||
                                OutputStream.class.isAssignableFrom(paramType) ||
                                ServletResponse.class.isAssignableFrom(paramType) ||
                                Writer.class.isAssignableFrom(paramType));
                    }

                    @Override
                    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
                        if (mavContainer != null) {
                            mavContainer.setRequestHandled(true);
                        }

                        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
                        Class<?> paramType = parameter.getParameterType();

                        if (ServletRequest.class.isAssignableFrom(paramType)) {
                            Object nativeRequest = webRequest.getNativeRequest(paramType);
                            if (nativeRequest == null) {
                                throw new IllegalStateException("Current request is not of type [" + paramType.getName() + "]: " + nativeRequest);
                            }
                            return nativeRequest;
                        } else if (ServletResponse.class.isAssignableFrom(paramType)) {
                            Object nativeResponse = webRequest.getNativeResponse(paramType);
                            if (nativeResponse == null) {
                                throw new IllegalStateException("Current response is not of type [" + paramType.getName() + "]: " + response);
                            }
                            return nativeResponse;
                        } else if (OutputStream.class.isAssignableFrom(paramType)) {
                            return response.getOutputStream();
                        } else if (Writer.class.isAssignableFrom(paramType)) {
                            return response.getWriter();
                        } else {
                            // should not happen
                            Method method = parameter.getMethod();
                            throw new UnsupportedOperationException("Unknown parameter type: " + paramType + " in method: " + method);
                        }
                    }
                })
                .addResolver(new AbstractNamedValueMethodArgumentResolver() {
                    @Override
                    protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
                        PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class);
                        return new NamedValueInfo(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
                    }

                    @Override
                    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
                        Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
                        return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
                    }

                    @Override
                    public boolean supportsParameter(MethodParameter parameter) {
                        if (!parameter.hasParameterAnnotation(PathVariable.class)) {
                            return false;
                        }
                        if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                            String paramName = parameter.getParameterAnnotation(PathVariable.class).value();
                            return StringUtils.hasText(paramName);
                        }
                        return true;
                    }
                });
    }


    protected void initHandlerMethods() {
        ReflectionUtils.MethodFilter methodFilter = method -> AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class);
        Set<Method> handlerMethods = MethodIntrospector.selectMethods(mBeanController.getClass(), methodFilter);
        for (Method method : handlerMethods) {
            HandlerMethod handlerMethod = new HandlerMethod(mBeanController, method);
            RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
            String[] pathArr = requestMapping.path();
            if (Utils.isNotEmpty(pathArr)) {
                for (String path : pathArr) {
                    handlerMethodMap.put(path, handlerMethod);
                }
            }
        }
        handlerMethods = MethodIntrospector.selectMethods(getClass(), methodFilter);
        for (Method method : handlerMethods) {
            HandlerMethod handlerMethod = new HandlerMethod(this, method);
            RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
            String[] pathArr = requestMapping.path();
            if (Utils.isNotEmpty(pathArr)) {
                for (String path : pathArr) {
                    handlerMethodMap.put(path, handlerMethod);
                }
            }
        }
    }

    public void dispatch(HttpServletRequest request, HttpServletResponse resp) throws IOException {
        String lookupPath = urlPathHelper.getLookupPathForRequest(request);
        if (request.getServletPath().equals(lookupPath)) lookupPath = "";
        HandlerMethod handlerMethod = null;
        String pathMatch = null;
        // RequestMapping 中的Path变量解析, 参考AbstractUrlHandlerMapping.buildPathExposingHandler()
        Map<String, String> uriTemplateVariables = new LinkedHashMap(5);
        for (Map.Entry<String, HandlerMethod> entry : handlerMethodMap.entrySet()) {
            // TODO 没有判断是GET或POST
            if (pathMatcher.match(entry.getKey(), lookupPath)) {
                handlerMethod = entry.getValue();
                pathMatch = entry.getKey();
                Map<String, String> vars = pathMatcher.extractUriTemplateVariables(entry.getKey(), lookupPath);
                uriTemplateVariables.putAll(vars);
                break;
            }
        }
        if (handlerMethod == null) {
            throw new RuntimeException("没有对应的 handler, path: " + lookupPath);
        }
        String pathWithinMapping = pathMatcher.extractPathWithinPattern(pathMatch, lookupPath);
        request.setAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);
        request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);

        ServletWebRequest webRequest = new ServletWebRequest(request, resp);
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        try {
            InvocableHandlerMethod invocableHandlerMethod = new InvocableHandlerMethod(handlerMethod);
            invocableHandlerMethod.setHandlerMethodArgumentResolvers(argumentResolvers);
            Object returnValue = invocableHandlerMethod.invokeForRequest(webRequest, mavContainer);
            mavContainer.getModel().put(PARAME_CONTEXTID, ctx.getId());
            mavContainer.getModel().put(ROOT_PATH, request.getServletPath());
            Class<?> returnType = handlerMethod.getReturnType().getParameterType();
            ResponseBody responseBody = handlerMethod.getMethodAnnotation(ResponseBody.class);
            if (responseBody == null) {
                // 返回 view 页面
                if (String.class.equals(returnType)) {
                    String viewName = (String) returnValue;
                    resp.setContentType("text/html");
                    StreamUtils.copy(
                            Utils.render(getHtmlTemplate(viewName), mavContainer.getModel()),
                            Charset.forName("utf-8"),
                            resp.getOutputStream()
                    );
                } else if (handlerMethod.isVoid()){
                    // ignore.
                } else {
                    log.info("not support return type: {}", returnValue);
                }
            } else {
                if (String.class.equals(returnType)) {
                    StreamUtils.copy((String) returnValue, Charset.defaultCharset(), resp.getOutputStream());
                } else if (handlerMethod.isVoid()){
                    // ignore.
                } else {
                    JSON.writeJSONString(resp.getOutputStream(), returnValue);
                }
            }

        } catch (Exception e) {
            log.error("dispatch error", e);
            //response.getWriter().write(e.getMessage());
        }
    }

    private String getHtmlTemplate(String pTemplateName) {
        if (templateCache.containsKey(pTemplateName)) templateCache.get(pTemplateName);
        Resource resource = ctx.getResource(internalStaticResourcePathPrefix + pTemplateName + ".html");
        String htmlTemplate = "";
        try {
            htmlTemplate = Utils.toString(resource.getInputStream());
            templateCache.put(pTemplateName, htmlTemplate);
        } catch (Exception e) {
            log.error("get html resource error, resource name: " + pTemplateName, e);
        }
        return htmlTemplate;
    }

    /**
     * 处理静态资源.
     */
    @GetMapping("/static/**")
    private void staticResource(HttpServletRequest req, HttpServletResponse resp) {
        String resourcePath = (String) req.getAttribute(RequestDispatcher.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
        Resource resource = ctx.getResource(internalStaticResourcePathPrefix + resourcePath);

//        InputStream in = MBeanConfiguration.class.getResourceAsStream("view/" + resourcePath);
        InputStream in = null;
        try {
            if (resourcePath.endsWith(".html")) {
                Map model = new HashMap(1);
                model.put(ROOT_PATH, req.getServletPath());
                StreamUtils.copy(
                        Utils.render(StreamUtils.copyToString(resource.getInputStream(), Charset.forName("utf-8")), model),
                        Charset.forName("utf-8"),
                        resp.getOutputStream()
                );
                return;
            }
            resp.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");

            if (new ServletWebRequest(req, resp).checkNotModified(resource.lastModified())) {
                // http: 1.1
                resp.setHeader("Cache-Control", CacheControl.maxAge(10, TimeUnit.MINUTES).getHeaderValue());
                log.trace("Resource not modified - returning 304");
                return;
            }
            if (resourcePath.endsWith(".js")) resp.setContentType("text/javascript");
            else if (resourcePath.endsWith(".css")) resp.setContentType("text/css");
            in = resource.getInputStream();
            StreamUtils.copy(in, resp.getOutputStream());

        } catch (Exception e) {
            log.error("get resource: " + resourcePath + " error", e);
        } finally {
            try {
//                resource.getInputStream().close();
                if (in != null) in.close();
            } catch (Throwable ex) {
                // ignore, see SPR-12999
            }
        }
    }


}
